<?php
/**
 * @version $Id: Heatmap.class.php 5 2011-08-06 19:24:23Z edo888@gmail.com $
 *
 * ClickHeat: Classe de génération des cartes / Maps generation class
 *
 * Cette classe est VOLONTAIREMENT écrite pour PHP 4
 * This class is VOLUNTARILY written for PHP 4
 *
 * @author Yvan Taviaud - LabsMedia - www.labsmedia.com
 * @since 08/05/2007
 * */

defined('_JEXEC') or die('Restricted access');

class Heatmap
{
    /* @var integer $memory Limite de mémoire / Memory limit */

    var $memory = 8388608;
    /* @var integer $step Groupement des pixels / Pixels grouping */
    var $step = 5;
    /* @var integer $startStep */
    var $startStep;
    /* @var integer $dot Taille des points de chaleur / Heat dots size */
    var $dot = 19;
    /* @var boolean $heatmap Affichage sous forme de carte de température / Show as heatmap */
    var $heatmap = true;
    /* @var boolean $palette Correctif pour la gestion de palette (cas des carrés rouges) / Correction for palette (in case of red squares) */
    var $palette = false;
    /* @var boolean $alpha Valeur de transparence de l'image (par défaut pas de transparence) / Alpha level (default is no alpha) */
    var $alpha = 0;
    /* @var boolean $rainbow Affichage de l'arc-en-ciel / Show rainbow */
    var $rainbow = true;
    /* @var boolean $copyleft Affichage du copyleft / Show copyleft */
    var $copyleft = true;
    /* @var integer $width Largeur de l'image / Image width */
    var $width;
    /* @var integer $height Hauteur de l'image / Image height */
    var $height;
    /* @var integer $maxClicks Nombre de clics maximum (sur 1 pixel) / Maximum clicks (on 1 pixel) */
    var $maxClicks;
    /* @var integer $maxY Hauteur maximale (point le plus bas) / Maximum height (lowest point) */
    var $maxY;
    /* @var resource $image Ressource image / Image resource */
    var $image;
    /* @var string $file Nom du fichier image (incluant le %d) / Image filename (including %d) */
    var $file;
    /* @var string $path Chemin du fichier image / Image path */
    var $path;
    /* @var string $cache Chemin du cache / Cache path */
    var $cache;
    /* @var string $error Erreur / Error */
    var $error = '';
    /* @var array $__colors Niveaux de dégradé (de 0 à 127) / Gradient levels (from 0 to 127) */
    var $__colors = array(50, 70, 90, 110, 120);
    /* @var integer $__low Niveau minimal de couleur RVB / Lower RGB level of color */
    var $__low = 0;
    /* @var integer $__high Niveau maximal de couleur RVB / Higher RGB level of color */
    var $__high = 255;
    /* @var integer $__grey Niveau du gris (couleur du 0 clic) / Grey level (color of no-click) */
    var $__grey = 240;

    function Heatmap()
    {
        $this->alpha = min($this->alpha, 127);
    }

    function generate($width, $height = 0)
    {
        /* First check paths */
        $this->path = rtrim($this->path, '/').'/';
        $this->cache = rtrim($this->cache, '/').'/';
        $this->file = str_replace('/', '', $this->file);
        if (!is_dir($this->path) || $this->path === '/')
        {
            return $this->raiseError('path = "'.$this->path.'" is not a directory or is "/"');
        }
        if (!is_dir($this->cache) || $this->cache === '/')
        {
            return $this->raiseError('cache = "'.$this->cache.'" is not a directory or is "/"');
        }
        if (strpos($this->file, '%d') === false)
        {
            return $this->raiseError('file = "'.$this->file.'" doesn\'t include a \'%d\' for image number');
        }

        $files = array('filenames' => array(), 'absolutes' => array()); /* Generated files list */
        $this->startStep = (int) floor(($this->step - 1) / 2);
        $nbOfImages = 1; /* Will be modified after the first image is created */
        $this->maxClicks = 1; /* Must not be zero for divisions */
        $this->maxY = 0;
        /**
         * Memory consumption:
         * imagecreate  : about 200,000 + 5 * $width * $height bytes
         * dots         : about 6,000 + 360 * DOT_WIDTH bytes each (100 dots)
         * imagepng     : about 4 * $width * $height bytes
         * So a rough idea of the memory is 10 * $width * $height + 500,000 (2 images) + 100 * (DOT_WIDTH * 360 + 6000)
         * */
        $this->width = (int) abs($width);
        if ($this->width === 0)
        {
            return $this->raiseError('Width can\'t be 0');
        }
        $height = (int) abs($height);
        if ($height === 0)
        {
            /* Calculating height from memory consumption, and add a 100% security margin: 10 => 20 */
            $this->height = floor(($this->memory - 500000 - 100 * ($this->dot * 360 + 6000)) / (20 * $width));
            /* Limit height to 1000px max, with a modulo of 10 */
            $this->height = (int) max(100, min(1000, $this->height - $this->height % 10));
        }
        else
        {
            /* Force height */
            $this->height = $height;
        }

        /* Startup tasks */
        if ($this->startDrawing() === false)
        {
            return false;
        }
        $files['width'] = $this->width;
        $files['height'] = $this->height;
        for ($image = 0; $image < $nbOfImages; $image++)
        {
            /* Image creation */
            $this->image = imagecreatetruecolor($this->width, $this->height);
            if ($this->heatmap === false)
            {
                $grey = imagecolorallocate($this->image, $this->__grey, $this->__grey, $this->__grey);
                imagefill($this->image, 0, 0, $grey);
            }
            else
            {
                /* Image is filled in the color "0", which means 0 click */
                imagefill($this->image, 0, 0, 0);
            }

            /* Draw next pixels for this image */
            if ($this->drawPixels($image) === false)
            {
                return false;
            }
            if ($image === 0)
            {
                if ($this->maxY === 0)
                {
                    if (defined('LANG_ERROR_DATA') === true)
                    {
                        return $this->raiseError(LANG_ERROR_DATA);
                    }
                    else
                    {
                        $this->maxY = 1;
                    }
                }
                $nbOfImages = (int) ceil($this->maxY / $this->height);
                $files['count'] = $nbOfImages;
            }

            if ($this->heatmap === true)
            {
                imagepng($this->image, sprintf($this->cache.$this->file.'_temp', $image));
            }
            else
            {
                /* "No clicks under this line" message */
                if ($image === $nbOfImages - 1 && defined('LANG_NO_CLICK_BELOW') === true)
                {
                    $black = imagecolorallocate($this->image, 0, 0, 0);
                    imageline($this->image, 0, $this->height - 1, $this->width, $this->height - 1, $black);
                    imagestring($this->image, 1, 1, $this->height - 9, LANG_NO_CLICK_BELOW, $black);
                }
                imagepng($this->image, sprintf($this->path.$this->file, $image));
            }
            imagedestroy($this->image);

            /* Result files */
            $files['filenames'][] = sprintf($this->file, $image);
            $files['absolutes'][] = sprintf($this->path.$this->file, $image);
        }
        /* End tasks */
        if ($this->finishDrawing() === false)
        {
            return false;
        }

        if ($this->heatmap === false)
        {
            return $files;
        }
        /* Now, our image is a direct representation of the clicks on each pixel, so create some fuzzy dots to put a nice blur effect if user asked for a heatmap */
        for ($i = 0; $i < 128; $i++)
        {
            $dots[$i] = imagecreatetruecolor($this->dot, $this->dot);
            imagealphablending($dots[$i], false);
        }
        for ($x = 0; $x < $this->dot; $x++)
        {
            for ($y = 0; $y < $this->dot; $y++)
            {
                $sinX = sin($x * pi() / $this->dot);
                $sinY = sin($y * pi() / $this->dot);
                for ($i = 0; $i < 128; $i++)
                {
                    $alpha = 127 - $i * $sinX * $sinY * $sinX * $sinY;
                    imagesetpixel($dots[$i], $x, $y, ((int) $alpha) * 16777216);
                }
            }
        }

        /**
         * Colors creation:
         * grey => deep blue (rgB)  => light blue (rGB) => green (rGb)      => yellow (RGb)     => red (Rgb)
         * 0       $this->__colors[0]      $this->__colors[1]      $this->__colors[2]      $this->__colors[3]      128
         * */
        sort($this->__colors);
        $colors = array();
        for ($i = 0; $i < 128; $i++)
        {
            /* Red */
            if ($i < $this->__colors[0])
            {
                $colors[$i][0] = $this->__grey + ($this->__low - $this->__grey) * $i / $this->__colors[0];
            }
            elseif ($i < $this->__colors[2])
            {
                $colors[$i][0] = $this->__low;
            }
            elseif ($i < $this->__colors[3])
            {
                $colors[$i][0] = $this->__low + ($this->__high - $this->__low) * ($i - $this->__colors[2]) / ($this->__colors[3] - $this->__colors[2]);
            }
            else
            {
                $colors[$i][0] = $this->__high;
            }
            /* Green */
            if ($i < $this->__colors[0])
            {
                $colors[$i][1] = $this->__grey + ($this->__low - $this->__grey) * $i / $this->__colors[0];
            }
            elseif ($i < $this->__colors[1])
            {
                $colors[$i][1] = $this->__low + ($this->__high - $this->__low) * ($i - $this->__colors[0]) / ($this->__colors[1] - $this->__colors[0]);
            }
            elseif ($i < $this->__colors[3])
            {
                $colors[$i][1] = $this->__high;
            }
            else
            {
                $colors[$i][1] = $this->__high - ($this->__high - $this->__low) * ($i - $this->__colors[3]) / (127 - $this->__colors[3]);
            }
            /* Blue */
            if ($i < $this->__colors[0])
            {
                $colors[$i][2] = $this->__grey + ($this->__high - $this->__grey) * $i / $this->__colors[0];
            }
            elseif ($i < $this->__colors[1])
            {
                $colors[$i][2] = $this->__high;
            }
            elseif ($i < $this->__colors[2])
            {
                $colors[$i][2] = $this->__high - ($this->__high - $this->__low) * ($i - $this->__colors[1]) / ($this->__colors[2] - $this->__colors[1]);
            }
            else
            {
                $colors[$i][2] = $this->__low;
            }
        }
        for ($image = 0; $image < $nbOfImages; $image++)
        {
            $img = imagecreatetruecolor($this->width, $this->height);
            $white = imagecolorallocate($img, 255, 255, 255);
            /* «imagefill» doesn't work correctly on some hosts, ending on a red drawing */
            imagefilledrectangle($img, 0, 0, $this->width - 1, $this->height - 1, $white);
            imagealphablending($img, true);

            $imgSrc = @imagecreatefrompng(sprintf($this->cache.$this->file.'_temp', $image));
            @unlink(sprintf($this->cache.$this->file.'_temp', $image));
            if ($imgSrc === false)
            {
                return $this->raiseError('::MEMORY_OVERFLOW::');
            }
            for ($x = $this->startStep; $x < $this->width; $x += $this->step)
            {
                for ($y = $this->startStep; $y < $this->height; $y += $this->step)
                {
                    $dot = (int) ceil(imagecolorat($imgSrc, $x, $y) / $this->maxClicks * 100);
                    if ($dot !== 0)
                    {
                        imagecopy($img, $dots[$dot], ceil($x - $this->dot / 2), ceil($y - $this->dot / 2), 0, 0, $this->dot, $this->dot);
                    }
                }
            }
            /* Destroy image source */
            imagedestroy($imgSrc);

            /* Rainbow */
            if ($image === 0 && $this->rainbow === true)
            {
                for ($i = 1; $i < 128; $i += 2)
                {
                    /* Erase previous alpha channel so that clicks don't change the heatmap by combining their alpha */
                    imageline($img, ceil($i / 2), 0, ceil($i / 2), 10, 16777215);
                    /* Then put our alpha */
                    imageline($img, ceil($i / 2), 0, ceil($i / 2), 10, (127 - $i) * 16777216);
                }
            }

            if ($this->alpha === 0)
            {
                /* Some version of imagetruecolortopalette() don't transform alpha value to non alpha */
                if ($this->palette === true)
                {
                    for ($x = 0; $x < $this->width; $x++)
                    {
                        for ($y = 0; $y < $this->height; $y++)
                        {
                            /* Get Alpha value (0->127) and transform it to red (divide color by 16777216 and multiply by 65536 * 2 (red is 0->255), so divide it by 128) */
                            imagesetpixel($img, $x, $y, (imagecolorat($img, $x, $y) & 0x7F000000) / 128);
                        }
                    }
                }

                /* Change true color image to palette then change palette colors */
                imagetruecolortopalette($img, false, 127);
                for ($i = 0, $max = imagecolorstotal($img); $i < $max; $i++)
                {
                    $color = imagecolorsforindex($img, $i);
                    imagecolorset($img, $i, $colors[floor(127 - $color['red'] / 2)][0], $colors[floor(127 - $color['red'] / 2)][1], $colors[floor(127 - $color['red'] / 2)][2]);
                }
            }
            else
            {
                /* Need some transparency, really? So we have to deal with each and every pixel */
                imagealphablending($img, false);
                imagesavealpha($img, true);
                for ($x = 0; $x < $this->width; $x++)
                {
                    for ($y = 0; $y < $this->height; $y++)
                    {
                        /* Get blue value (0->255), divide it by 2, and apply transparency + colors */
                        $blue = floor((imagecolorat($img, $x, $y) & 0xFF) / 2);
                        imagesetpixel($img, $x, $y, floor($this->alpha * $blue / 127) * 16777216 + $colors[127 - $blue][0] * 65536 + $colors[127 - $blue][1] * 256 + $colors[127 - $blue][2]);
                    }
                }
            }

            $grey = imagecolorallocate($img, $this->__grey, $this->__grey, $this->__grey);
            $gray = imagecolorallocate($img, ceil($this->__grey / 2), ceil($this->__grey / 2), ceil($this->__grey / 2));
            $white = imagecolorallocate($img, 255, 255, 255);
            $black = imagecolorallocate($img, 0, 0, 0);

            /* maxClicks */
            if ($image === 0 && $this->rainbow === true)
            {
                imagerectangle($img, 0, 0, 65, 11, $white);
                imagefilledrectangle($img, 0, 11, 65, 18, $white);
                imagestring($img, 1, 0, 11, '0', $black);
                $right = 66 - strlen($this->maxClicks) * 5;
                imagestring($img, 1, $right, 11, $this->maxClicks, $black);
                imagestring($img, 1, floor($right / 2) - 12, 11, 'clicks', $black);
            }

            if ($image === $nbOfImages - 1)
            {
                /* "No clicks under this line" message */
                if (defined('LANG_NO_CLICK_BELOW') === true)
                {
                    imageline($img, 0, $this->height - 1, $this->width, $this->height - 1, $gray);
                    imagestring($img, 1, 1, $this->height - 9, LANG_NO_CLICK_BELOW, $gray);
                }
                /* Copyleft */
                if ($this->copyleft === true)
                {
                    imagestring($img, 1, $this->width - 160, $this->height - 9, 'Open source heatmap by ClickHeat', $grey);
                    imagestring($img, 1, $this->width - 161, $this->height - 9, 'Open source heatmap by ClickHeat', $gray);
                }
            }

            /* Save PNG file */
            imagepng($img, sprintf($this->path.$this->file, $image));
            imagedestroy($img);
        }
        for ($i = 0; $i < 100; $i++)
        {
            imagedestroy($dots[$i]);
        }
        return $files;
    }

    /**
     * Retourne une erreur / Returns an error
     *
     * @param string $error
     * */
    function raiseError($error)
    {
        $this->error = $error;
        return false;
    }

}

?>